﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.StorageClient;

namespace jp110311.StorageModels
{
    public static class UserMessagingService
    {
        public static ICacheService CacheService = new WebCacheWrapper();

        private const string MessagesTableName = "messages";
        private static bool _tableCreated;
        private static readonly char[] IndexItemSeparator = new[] { ';' };
        private const string AddressBookTableName = "addrbook";


        public static void SendMessage(string sender, DateTime sendAt, string toUserList, string title, string content)
        {
            string[] toUsers = toUserList.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);

            foreach (string toUser in toUsers)
            {
                SendMessageToUser( toUser, sendAt, sender, title, content);
            }
        }

        private static void SendMessageToUser(string userName, DateTime sendAt ,string sender, string title, string content)
        {
            TableServiceContext context = GetWritableDataServiceContext();

            var userMessagingEntity = new UserMessagingEntity(sender,sendAt,userName,title,content)
                                      {
                                          Unread = true
                                      };
            context.AddObject( MessagesTableName, userMessagingEntity );
            var indexEntity = GetMessageIndex(context, userName);
            if (indexEntity == null)
            {
                indexEntity = new UserMessagingIndex(userName)
                              {
                                  AllItems = userMessagingEntity.RowKey,
                                  UnreadItems = userMessagingEntity.RowKey
                              };
                context.AddObject(MessagesTableName,indexEntity );
            }
            else
            {
                List<string> unreadItems = indexEntity.UnreadItems.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries).ToList();
                unreadItems.Add(userMessagingEntity.RowKey);
                indexEntity.UnreadItems = string.Join(";", unreadItems);
                indexEntity.AllItems += ";" + userMessagingEntity.RowKey;

                context.UpdateObject(indexEntity);
            }
            DoSaveChange(context);
        }

        internal static bool UseDevelopmentStorage { get; set; }

        private static readonly Lazy<EntityServiceUtility> ServiceUtility = 
            new Lazy<EntityServiceUtility>(
                () =>
                {
                    var serviceUtility = new EntityServiceUtility(GetAccount())
                           {
                               SuppressBatch = UseDevelopmentStorage
                           };
                    serviceUtility.ContextCreated += ServiceUtilityContextCreated;
                    return serviceUtility;
                });

        static void ServiceUtilityContextCreated(object sender, DataServiceContextCreatedEventArgs e)
        {
            if (sender == null) throw new ArgumentNullException("sender");
            if (_tableCreated) return;

            var s = sender as EntityServiceUtility;
            if (s == null) throw new ArgumentOutOfRangeException("sender");
            var client = s.GetTableClient();
            client.CreateTableIfNotExist(MessagesTableName);
            client.CreateTableIfNotExist(AddressBookTableName);
            _tableCreated = true;
        }

        private static TableServiceContext GetDataServiceContext()
        {
            return ServiceUtility.Value.GetTableServiceContext();
        }

        private static TableServiceContext GetWritableDataServiceContext()
        {
            return ServiceUtility.Value.GetTableServiceContext();
        }

        private static CloudStorageAccount GetAccount()
        {
            if (UseDevelopmentStorage) return CloudStorageAccount.DevelopmentStorageAccount;
            return CloudStorageAccount.FromConfigurationSetting("StorageAccountConnection");
        }

        private static string GetPrevItem(string[] items, string id)
        {
            int itemIndex = Array.IndexOf(items, id);
            if (itemIndex == -1) return null;

            if (itemIndex == 0) return null;
            return items[itemIndex -1 ];
        }

        private static string GetNextItem(string[] items, string id)
        {
            int itemIndex = Array.IndexOf(items, id);
            if (itemIndex == -1) return null;
            if (itemIndex == items.Length - 1) return null;
            return items[itemIndex + 1];
        }

        public static string GetNextMessageId(string userName, string id)
        {
            var context = GetDataServiceContext();
            var index = GetMessageIndexWithCache(context, userName);
            var items = index.AllItems.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            Array.Sort(items);
            return GetNextItem(items, id);
        }

        public static string GetPrevMessageId(string userName, string id)
        {
            var context = GetDataServiceContext();
            var index = GetMessageIndexWithCache(context, userName);
            var items = index.AllItems.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            Array.Sort(items);
            return GetPrevItem(items, id);
        }

        public static string GetNextUnreadMessageId(string userName, string id)
        {
            var context = GetDataServiceContext();
            var index = GetMessageIndexWithCache(context, userName);
            var items = index.UnreadItems.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            Array.Sort(items);
            return GetNextItem(items, id);
        }

        public static string GetPrevUnreadMessageId(string userName, string id)
        {
            var context = GetDataServiceContext();
            var index = GetMessageIndexWithCache(context, userName);
            var items = index.UnreadItems.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            Array.Sort(items);
            return GetPrevItem(items, id);
        }

        public static string GetNextImportanceMessageId(string userName, string id)
        {
            var context = GetDataServiceContext();
            var index = GetMessageIndexWithCache(context, userName);
            var items = index.ImportanceItems.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            Array.Sort(items);
            return GetNextItem(items, id);
        }

        public static string GetPrevImportanceMessageId(string userName, string id)
        {
            var context = GetDataServiceContext();
            var index = GetMessageIndexWithCache(context, userName);
            var items = index.ImportanceItems.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            Array.Sort(items);
            return GetPrevItem(items, id);
        }

        [CLSCompliant(false)]
        public static UserMessagingEntity GetMessage(string userName, string id)
        {
            var context = GetDataServiceContext();
            return FetchItemsWithCache(context, userName, new[] {id}).SingleOrDefault();
        }

        public static int CountMessage(string userName)
        {
            var context = GetDataServiceContext();
            var index = GetMessageIndexWithCache(context, userName);
            var items = index.AllItems.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            return items.Length;
        }

        public static IEnumerable<UserMessagingEntity> ListMessage( string userName,int skipCount, int maxCount )
        {
            var context = GetDataServiceContext();
            var index = GetMessageIndexWithCache(context, userName);
            var items = index.AllItems.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries)
                .Skip(skipCount)
                .Take(maxCount);
            return FetchItemsWithCache(context, userName, items);
        }

        public static int CountUnreadMessage(string userName)
        {
            var context = GetDataServiceContext();
            var index = GetMessageIndexWithCache(context, userName);
            if (index == null) return 0;
            return index.UnreadItems
                .Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries).Length;
        }



        public static IEnumerable<UserMessagingEntity> ListUnreadMessage(string userName, int skipCount, int maxCount)
        {
            var context = GetDataServiceContext();
            var index = GetMessageIndexWithCache(context, userName);
            if (index == null) return Enumerable.Empty<UserMessagingEntity>();
            string[] unreads = index.UnreadItems.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            string[] fetchItems = unreads.Skip(skipCount).Take(maxCount).ToArray();
            return FetchItemsWithCache(context, userName, fetchItems);
        }

        private static UserMessagingIndex GetMessageIndex(TableServiceContext context, string userName)
        {
            var qidx = context.CreateQuery<UserMessagingIndex>(MessagesTableName);
            return qidx.Where(i => i.PartitionKey == userName && i.RowKey == "INDEX")
                .SingleOrDefault();
        }


        private static UserMessagingIndex GetMessageIndexWithCache(TableServiceContext context, string userName)
        {
            // キャッシュ利用
            string cacheKey = "MSGINDEX/" + userName;
            var cachedIndex = CacheService.Get(cacheKey) as CacheItem<UserMessagingIndex>;
            if( cachedIndex==null )
            {
                cachedIndex = new CacheItem<UserMessagingIndex>
                              {
                                  Value = GetMessageIndex(context, userName)
                              };
                CacheService.Insert(cacheKey,cachedIndex);
            }
            return cachedIndex.Value;
        }

        static readonly TableStoragePredicateUtility<UserMessagingEntity> UserMessagingPredUtil 
            = new TableStoragePredicateUtility<UserMessagingEntity>();

        private static IEnumerable<UserMessagingEntity> FetchItems(TableServiceContext context, string userName, IEnumerable<string> fetchItems)
        {
            IQueryable<UserMessagingEntity> query = context.CreateQuery<UserMessagingEntity>(MessagesTableName);
            var pred = UserMessagingPredUtil.BuildLambda(userName, fetchItems);
            if (pred == null) return Enumerable.Empty<UserMessagingEntity>();
            return query.Where(pred).AsEnumerable();
        }


        private static IEnumerable<UserMessagingEntity> FetchItemsWithCache(TableServiceContext context, string userName, IEnumerable<string> fetchItems)
        {
            // キャッシュ利用
            string cacheKey = "MSGS/" + userName + "/" + string.Join(";", fetchItems);
            var cachedItems = CacheService.Get(cacheKey) as CacheItem<IEnumerable<UserMessagingEntity>>;
            if( cachedItems==null )
            {
                cachedItems = new CacheItem<IEnumerable<UserMessagingEntity>>
                              {
                                  Value = FetchItems(context, userName, fetchItems)
                              };
                CacheService.Insert(cacheKey,cachedItems);
            }
            return cachedItems.Value;
        }

        public static int CountImportanceMessage(string userName)
        {
            var context = GetDataServiceContext();
            var index = GetMessageIndexWithCache(context, userName);
            if (index == null) return 0;
            string[] importance = index.ImportanceItems.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            return importance.Length;
        }

        public static IEnumerable<UserMessagingEntity> ListImportanceMessage( string userName, int skipCount,int maxCount )
        {
            var context = GetDataServiceContext();
            var index = GetMessageIndexWithCache(context, userName);
            if (index == null) return Enumerable.Empty<UserMessagingEntity>();
            string[] importance = index.ImportanceItems.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            string[] fetchItems = importance.Skip(skipCount).Take(maxCount).ToArray();
            return FetchItemsWithCache(context, userName, fetchItems);
        }

        public static void MarkMessageToRead( string userName, string messageRowKey )
        {
            var context = GetWritableDataServiceContext();
            var message = FetchItems(context, userName, new[] { messageRowKey }).SingleOrDefault();
            if (message == null) return;
            message.Unread = false;
            context.UpdateObject(message);

            var index = GetMessageIndex(context, userName);
            if (index == null) return;
            if (index.UnreadItems == null) return;
            string[] unreads = index.UnreadItems.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            index.UnreadItems = string.Join(";", unreads.Where(mKey => mKey != messageRowKey));
            context.UpdateObject(index);
            DoSaveChange(context);
        }

        public static void MarkMessageImportance( string userName ,string messageRowKey )
        {
            var context = GetWritableDataServiceContext();
            var message = FetchItems(context, userName, new[] {messageRowKey}).SingleOrDefault();
            if( message==null ) return;
            message.Impotance = true;
            context.UpdateObject(message);
            
            var index = GetMessageIndex(context, userName);
            if (index == null)
            {
                index = new UserMessagingIndex(userName)
                        {
                            ImportanceItems = messageRowKey
                        };
                context.AddObject(MessagesTableName, index);
                context.SaveChanges();
                return;
            }
            if ( string.IsNullOrEmpty( index.ImportanceItems ) )
            {
                index.ImportanceItems = messageRowKey;
                context.UpdateObject(index);
                context.SaveChanges();
                return;
            }
            index.ImportanceItems += ";" + messageRowKey;
            context.UpdateObject(index);
            DoSaveChange(context);
        }

        public  static void UnmarkMessageImportance( string userName , string messageRowKey )
        {
            var context = GetWritableDataServiceContext();
            var message = FetchItems(context, userName, new[] { messageRowKey }).SingleOrDefault();
            if (message == null) return;
            message.Impotance = false;

            var index = GetMessageIndex(context, userName);
            if (index == null) return;
            if (index.ImportanceItems == null) return;
            string[] importance = index.ImportanceItems.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            index.ImportanceItems = string.Join(";", importance.Where(mKey => mKey != messageRowKey));
            context.UpdateObject(index);
            DoSaveChange(context);
        }

        private static void DoSaveChange(TableServiceContext context)
        {
            ServiceUtility.Value.DoSaveChange(context);
        }

        public static void DeleteMessage( string userName, string messageRowKey )
        {
            throw new NotImplementedException();
        }

        private static readonly TableStoragePredicateUtility<UserAddressBookEntity> AddrBookPredicateUtil 
            = new TableStoragePredicateUtility<UserAddressBookEntity>();


        public static IEnumerable<UserAddressBookEntity> GetAddressBookItems( string userName,int skipCount, int maxCount )
        {
            var context = GetDataServiceContext();
            var qaddrBook = context.CreateQuery<UserAddressBookEntity>(AddressBookTableName);

            UserAddressBookIndexEntity addrIndex = GetAddressBookIndexWithCache(context, userName);
            if (addrIndex == null) return Enumerable.Empty<UserAddressBookEntity>();

            var rowKeys = addrIndex.ItemsRowKeys
                .Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries)
                .Skip(skipCount)
                .Take(maxCount);

            var pred =AddrBookPredicateUtil.BuildLambda(userName, rowKeys);
            if (pred == null) return Enumerable.Empty<UserAddressBookEntity>();

            return qaddrBook
                .Where(pred).AsEnumerable();
        }

        public static int CountAddressBookItems(string userName)
        {
            var context = GetDataServiceContext();
            UserAddressBookIndexEntity addrIndex = GetAddressBookIndexWithCache(context, userName);
            if (addrIndex == null) return 0;
            return addrIndex.ItemsRowKeys.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries).Count();
        }

        private static UserAddressBookIndexEntity GetAddressBookIndex(TableServiceContext context, string userName)
        {
            var qidx = context.CreateQuery<UserAddressBookIndexEntity>(AddressBookTableName);
            return qidx.Where(ix => ix.PartitionKey == userName && ix.RowKey == "ADDRINDEX").SingleOrDefault();
        }

        private static UserAddressBookIndexEntity GetAddressBookIndexWithCache(TableServiceContext context, string userName)
        {
            // キャッシュ利用OK
            string cacheKey = "ADDRBOOKIX/" + userName;
            var cachedIndex = CacheService.Get(cacheKey) as CacheItem<UserAddressBookIndexEntity>;
            if(cachedIndex==null)
            {
                cachedIndex = new CacheItem<UserAddressBookIndexEntity>
                              {
                                  Value = GetAddressBookIndex(context, userName)
                              };
                CacheService.Insert(cacheKey,cachedIndex);
            }
            return cachedIndex.Value;
        }

        public static int CountAddressBookItemsByTag(string userName, string tag)
        {
            var context = GetDataServiceContext();
            var qaddrBookTag = context.CreateQuery<UserAddressBookTagEntity>(AddressBookTableName);
            string tagKey = "ADDRTAG0" + tag;
            var addrBookTag = qaddrBookTag
                .Where(tagEntity => tagEntity.PartitionKey == userName && tagEntity.RowKey == tagKey)
                .SingleOrDefault();
            if (addrBookTag == null) return 0;
            var tagedUsers = addrBookTag.UserList.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            return tagedUsers.Length;
        }

        public static IEnumerable<UserAddressBookEntity> GetAddressBookItemsByTag(string userName, string tag, int skipCount, int maxCount)
        {
            var context = GetDataServiceContext();
            var qaddrBookTag = context.CreateQuery<UserAddressBookTagEntity>(AddressBookTableName);
            string tagKey = "ADDRTAG0" + tag;
            var addrBookTag = qaddrBookTag
                .Where(tagEntity => tagEntity.PartitionKey == userName && tagEntity.RowKey==tagKey )
                .SingleOrDefault();
            if (addrBookTag == null) return Enumerable.Empty<UserAddressBookEntity>();
            var tagedUsers = addrBookTag.UserList.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            tagedUsers = tagedUsers.Skip(skipCount).Take(maxCount).ToArray();

            var pred=  AddrBookPredicateUtil.BuildLambda(userName, tagedUsers.Select(tagedUser => "ADDRBOOK0" + tagedUser));

            if (pred == null) return Enumerable.Empty<UserAddressBookEntity>();
            var qaddrBook = context.CreateQuery<UserAddressBookEntity>(AddressBookTableName);
            return qaddrBook.Where(pred).AsEnumerable();
        }

        public static void AddressBookAdd( string userName, string[] addUsers, string memo, string[] tag )
        {
            var context = GetWritableDataServiceContext();
            var qaddrBook = context.CreateQuery<UserAddressBookEntity>(AddressBookTableName);
            var qaddrTag = context.CreateQuery<UserAddressBookTagEntity>(AddressBookTableName);
            var booked = qaddrBook.Where(e => e.PartitionKey == userName 
                 && string.Compare( e.RowKey, "ADDRBOOK0")>0
                 && string.Compare( e.RowKey, "ADDRBOOK1")<0 ).ToList();
            var tagIndex = qaddrTag
                .Where(e => e.PartitionKey == userName
                            && string.Compare(e.RowKey, "ADDRTAG0") > 0
                            && string.Compare(e.RowKey, "ADDRTAG1") < 0)
                .ToList();

            var index = GetAddressBookIndex(context, userName);
            if (index == null)
            {
                index = new UserAddressBookIndexEntity(userName)
                        {
                            ItemsRowKeys = string.Join(";", addUsers.Select(u => "ADDRBOOK0" + u))
                        };
                context.AddObject(AddressBookTableName,index);
            }
            else
            {
                var items = index.ItemsRowKeys
                    .Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries)
                    .ToList();
                bool userAdded = false;
                foreach (var addUser in addUsers)
                {
                    if (items.Contains("ADDRBOOK0" + addUser)) continue;
                    items.Add("ADDRBOOK0" + addUser);
                    userAdded = true;
                }
                if (userAdded)
                {
                    index.ItemsRowKeys = string.Join(";", items);
                    context.UpdateObject(index);
                }                
            }

            var readyBooked = new List<string>();
            foreach (var userAddressBookEntity in booked)
            {
                readyBooked.Add(userAddressBookEntity.BookedUser);
                if (!addUsers.Contains(userAddressBookEntity.BookedUser))
                {
                    continue;
                }
                userAddressBookEntity.Memo += memo;
                var userTagList = userAddressBookEntity.TagList
                    .Split(new[] {';'}, StringSplitOptions.RemoveEmptyEntries)
                    .ToList();
                foreach (var s in tag)
                {
                    if( userTagList.Contains(s)) continue;
                    userTagList.Add(s);
                }
                userAddressBookEntity.TagList = string.Join(";", userTagList);
                context.UpdateObject(userAddressBookEntity);
            }
            string tags = string.Join(";", tag);
            foreach (var addUser in addUsers)
            {
                if( readyBooked.Contains(addUser)) continue;
                var userAddressBookEntity = new UserAddressBookEntity(userName, addUser, memo, tags);
                context.AddObject(AddressBookTableName,userAddressBookEntity);
            }
            foreach (var addressBookTagEntity in tagIndex)
            {
                if (tags.Contains( addressBookTagEntity.TagName))
                {
                    var users = addressBookTagEntity.UserList.Split(IndexItemSeparator).ToList();
                    bool updated = false;
                    foreach (var addUser in addUsers)
                    {
                        if( users.Contains(addUser))continue;
                        users.Add(addUser);
                        updated = true;
                    }
                    if (updated)
                    {
                        addressBookTagEntity.UserList = string.Join(";", users);
                        context.UpdateObject(addressBookTagEntity);
                    }
                }
            }
            List<string> newTags = tag.Where(t => !tagIndex.Any(ti => ti.TagName == t)).ToList();
            string userList = string.Join(";", addUsers);
            foreach (var newTag in newTags)
            {
                var tagEntity = new UserAddressBookTagEntity(userName, newTag, userList);
                context.AddObject(AddressBookTableName, tagEntity);
            }
            DoSaveChange(context);
        }

        
        public static void AddressBookRemoveTag( string userName, string[] tagedUsers, string tag )
        {
            var context = GetWritableDataServiceContext();
            var qaddrBook = context.CreateQuery<UserAddressBookEntity>(AddressBookTableName);
            var qaddrTag = context.CreateQuery<UserAddressBookTagEntity>(AddressBookTableName);
            string tagKey = "ADDRTAG0" + tag;
            var tagEntity = qaddrTag
                .Where(te => te.PartitionKey == userName && te.RowKey == tagKey)
                .SingleOrDefault();
            var userList = tagEntity.UserList.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries).ToList();
            foreach (var tagedUser in tagedUsers)
            {
                if (userList.Contains(tagedUser))
                {
                    userList.Remove(tagedUser);

                    string addrKey = "ADDRBOOK0" + tagedUser;
                    var addrEntity = qaddrBook
                        .Where(addr => addr.PartitionKey == userName && addr.RowKey == addrKey)
                        .SingleOrDefault();
                    if (addrEntity == null) continue;
                    
                    var tagList = addrEntity.TagList.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries).ToList();
                    tagList.Remove(tag);
                    addrEntity.TagList = string.Join(";", tagList);
                    context.UpdateObject(addrEntity);
                }
            }
            DoSaveChange(context);
        }

        private static readonly TableStoragePredicateUtility<UserAddressBookTagEntity> AddrTagPredUtility = 
            new TableStoragePredicateUtility<UserAddressBookTagEntity>();

        public static void AddressBookRemoveEntry( string userName, string bookedUserName )
        {
            var context = GetWritableDataServiceContext();
            var qaddrBook = context.CreateQuery<UserAddressBookEntity>(AddressBookTableName);

            string addrKey = "ADDRBOOK0" + bookedUserName;
            var addrEntity = qaddrBook.Where(a => a.PartitionKey == userName && a.RowKey == addrKey).SingleOrDefault();
            if( addrEntity==null) return;

            string[] tags = addrEntity.TagList.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries);
            context.DeleteObject(addrEntity );

            var pred =AddrTagPredUtility.BuildLambda(userName, tags.Select(tag => "ADDRTAG0" + tag));
            if (pred != null)
            {
                var qaddrTag = context.CreateQuery<UserAddressBookTagEntity>(AddressBookTableName);
                var tagEntityList = qaddrTag.Where(pred).ToList();

                foreach (var userAddressBookTagEntity in tagEntityList)
                {
                    var tagedUsers = userAddressBookTagEntity.UserList.Split(IndexItemSeparator, StringSplitOptions.RemoveEmptyEntries).ToList();
                    tagedUsers.Remove(bookedUserName);
                    userAddressBookTagEntity.UserList = string.Join(";", tagedUsers);
                    context.UpdateObject(userAddressBookTagEntity);
                }
            }

            DoSaveChange(context);
        }

        public static IEnumerable<UserAddressBookTagEntity> GetAddressBookTags(string userName)
        {
            var context = GetDataServiceContext();
            var qaddrTag = context.CreateQuery<UserAddressBookTagEntity>(AddressBookTableName);
            return qaddrTag.Where(t => t.PartitionKey == userName
                            && string.Compare(t.RowKey, "ADDRTAG0") > 0
                            && string.Compare(t.RowKey, "ADDRTAG1") < 0).ToList();
        }
    }
}
